package monash.med.crystallography.DataSetTools.TarBzipper;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.tools.bzip2.CBZip2OutputStream;
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarOutputStream;

/**
 * TarBzipper by Steve Androulakis - Tar archives the supplied directory, then compresses it using the bzip2 algorithm
 * <p>
 * Use static method tarBzip(String sourceDir[, String destDir])
 * 
 * @author Steve Androulakis (Monash University)
 * @version 1.0
*/
public class TarBzipper
{

	private static String sourceDir;
	private static String tarName;
	private static String bzipName;
	private static String destDir;

	private static File initDirectory()
	{
		File sourceDirF = new File(TarBzipper.sourceDir);
		
		if(destDir == null)
		{
			tarName = sourceDirF.getAbsolutePath() + ".tar";
		}
		else
		{
			File destDirF = new File(TarBzipper.destDir);
			destDirF.mkdirs();
			tarName = destDirF.getAbsolutePath() + File.separator + sourceDirF.getName() + ".tar";
		}
		
		bzipName = tarName + ".bz";
		
		return new File(new File(sourceDir).getAbsolutePath());
	}
	
	/**
	 * @param  sourcePath The absolute or relative directory path to be archived and compressed
	 */
	public static String tarBzip(String sourceDir)
	{
		TarBzipper.sourceDir = sourceDir;
		tar();
		bzip();
		cleanup();
		
		return bzipName;
	}
	
	/**
	 * @param  sourcePath The absolute or relative directory path to be archived and compressed
	 * @param destinationPath The absolute or relative path for the destination .tar.bz
	 */
	public static String tarBzip(String sourceDir, String destDir)
	{
		TarBzipper.sourceDir = sourceDir;
		TarBzipper.destDir = destDir;
		tar();
		bzip();
		cleanup();
		
		return bzipName;
	}	

	private static String tar()
	{
		try
		{
			FileOutputStream stream;
			File dir = initDirectory();

			File archiveFile = new File(tarName);
			if (archiveFile.exists())
			{
				throw new Exception(archiveFile.getName() + " already exists. Exiting...");
			}
			stream = new FileOutputStream(archiveFile);
			TarOutputStream out = new TarOutputStream(stream);
			List<File> files = TarBzipper.getFileListing(dir);
			
			// print out all file names, and display the order of File.compareTo
			for (File file : files)
			{
				if (!file.isDirectory())
				{
					System.out.println("Added " + file.getAbsolutePath().substring(sourceDir.length())
							+ " to the archive.");
					byte[] buf = new byte[1024];
					int len;
					// Add archive entry
					TarEntry tarAdd = new TarEntry(file);
					tarAdd.setModTime(file.lastModified());
					tarAdd.setMode(TarEntry.LF_NORMAL);
					tarAdd.setName(file.getAbsolutePath().substring(sourceDir.length()));
					tarAdd.setMode(775);
					// Create a buffered input stream out of the file
					// we're trying to add into the Tar
					FileInputStream fin = new FileInputStream(file);
					BufferedInputStream in = new BufferedInputStream(fin);
                                        
					out.putNextEntry(tarAdd);
					// Read bytes from the file and write into the Tar.
					while ((len = in.read(buf)) != -1)
						out.write(buf, 0, len);
					// Close the input stream.
					in.close();
                                        fin.close();
					// Close this entry in the Tar stream.
					out.closeEntry();
				}
			}
			out.close();
			stream.close();
			System.out.println("Tar Archive created successfully.");
		}
		catch (FileNotFoundException e)
		{
			System.out.println("Target directory not found. Exiting...");
		}
		catch (IOException e)
		{
			System.out.println("ERROR: Failed or interrupted file operation");
		}
		catch (Exception e)
		{
			System.out.println(e.getMessage());
		}
		return tarName;
	}

	private static String bzip()
	{
		try
		{
			if (new File(bzipName).exists())
			{
				throw new Exception(bzipName + " already exists. Exiting...");
			}
			System.out.println("Bzip compressing...");
			CBZip2OutputStream bzout = null;
			BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(bzipName));
			bos.write('B'); // write header characters
			bos.write('Z');
			bzout = new CBZip2OutputStream(bos);
			// Create a buffered input stream out of the file
			// we're trying to add into the Tar
			FileInputStream in = new FileInputStream(tarName);
			// Transfer bytes from the input file
			// to the gzip output stream
			byte[] buffer = new byte[8024];
			int n = 0;
			while (-1 != (n = in.read(buffer)))
			{
				bzout.write(buffer, 0, n);
			}
			in.close();
			bzout.flush();
			bzout.close();
                        bos.close();
			System.out.println("Bzipped successfully to " + bzipName);
		}
		catch (FileNotFoundException e)
		{
			System.out.println(tarName + " not found for Bzipping. Exiting...");
		}
		catch (IOException e)
		{
			System.out.println("ERROR: Failed or interrupted file operation");
		}
		catch (Exception e)
		{
			System.out.println(e.getMessage());
		}
		return bzipName;
	}

	private static void cleanup()
	{
		new File(tarName).delete();
	}

	/**
	 * Recursively walk a directory tree and return a List of all Files found;
	 * the List is sorted using File.compareTo.
	 * 
	 * @param aStartingDir
	 *            is a valid directory, which can be read.
	 */
	static private List<File> getFileListing(File aStartingDir) throws FileNotFoundException
	{
		validateDirectory(aStartingDir);
		List<File> result = new ArrayList<File>();
		File[] filesAndDirs = aStartingDir.listFiles();
		List<File> filesDirs = Arrays.asList(filesAndDirs);
		for (File file : filesDirs)
		{
			result.add(file); // always add, even if directory
			if (!file.isFile())
			{
				// must be a directory
				// recursive call!
				List<File> deeperList = getFileListing(file);
				result.addAll(deeperList);
			}
		}
		Collections.sort(result);
		return result;
	}

	/**
	 * Directory is valid if it exists, does not represent a file, and can be
	 * read.
	 */
	static private void validateDirectory(File aDirectory) throws FileNotFoundException
	{
		if (aDirectory == null)
		{
			throw new IllegalArgumentException("Directory should not be null.");
		}
		if (!aDirectory.exists())
		{
			throw new FileNotFoundException("Directory does not exist: " + aDirectory);
		}
		if (!aDirectory.isDirectory())
		{
			throw new IllegalArgumentException("Is not a directory: " + aDirectory);
		}
		if (!aDirectory.canRead())
		{
			throw new IllegalArgumentException("Directory cannot be read: " + aDirectory);
		}
	}

	/**
	 * The commandline implementaton of the {@link TarBzipper} class.
	 * <p>
	 * Run <i>java -jar TarBzipper.jar</i> for instructions on usage.
	 */
	public static void main(String[] args)
	{
		if (args.length == 1)
		{
			tarBzip(args[0]);
		}
		else if (args.length == 2)
		{
			tarBzip(args[0], args[1]);
		}
		else
		{
			System.out
					.println("TarBzipper by Steve Androulakis - Creates a tar archive out of a given directory and its contents, then bzip compresses it");
			System.out.println("");
			System.out.println("USAGE:");
			System.out.println("TarBzipper <sourcePath> <destinationPath>");
			System.out.println("");
			System.out.println("sourcePath: The absolute or relative directory path to be archived and compressed");
			System.out.println("destinationPath (optional): The absolute or relative path for the destination .tar.bz ");
			System.out.println("   If left blank, file will be placed alongside <sourcePath>");
			System.out.println("Outputs <directoryname>.tar.bz alongside the specified directory in the file system");
		}
	}
}